package com.cellngine.server;

import java.net.Socket;
import java.util.List;
import java.util.Vector;

import com.cellngine.Constants;
import com.cellngine.JO;
import com.cellngine.Cellngine;
import com.cellngine.server.model.Session;

/**
 * ServerManager
 * 
 * Provides a simple API to configure cellngine's network I/O.
 * 
 * @author Felix Kirchmann <qwer@cellngine.com>
 */
public class ServerManager
{
	private ServerListener	serverPort;
	private InitialKeyPair	keyPair;
	private String			hubURL		= null;
	private List<Session>	clients;
	private int				maxPlayers	= Constants.default_maxplayers;
	private int				currentPort	= Constants.default_port;
	private int				nextPort	= Constants.default_port;
	private String			password	= null;
	
	/**
	 * Creates a new <code>Session</code> object and adds it to the internal list of connected
	 * clients.
	 * 
	 * @param socket
	 *            The socket belonging to this session.
	 * @return The created <code>Session</code>, or <code>null</code> if the creation failed.
	 */
	public synchronized Session createSession(final Socket socket)
	{
		if (this.clients == null)
		{
			this.clients = new Vector<Session>();
		}
		
		if (this.clients.size() >= this.maxPlayers)
		{
			Cellngine.log("Error: Unable to create ServerClient: maxPlayers reached or exceeded!",
					Constants.log_type.error);
			return null;
		}
		
		final ServerClientSocket cl_socket = new ServerClientSocket(this, socket);
		
		final Session session = new Session(cl_socket);
		this.clients.add(session);
		
		return session;
	}
	
	/**
	 * Removes a session from the internal list, freeing up the slot.
	 * This also terminates the connection (if this was not already done).
	 * 
	 * @param session
	 *            The session object to terminate.
	 */
	public synchronized void terminateSession(final Session session)
	{
		session.terminateConnection();
		if (this.clients.contains(session))
		{
			this.clients.remove(session);
		}
	}
	
	/**
	 * For thread safety, this function combines starting and stopping the network
	 * socket.
	 * 
	 * @param state
	 *            If <code>true</code>, the server will be started (unless it is already running),
	 *            if <code>false</code>, the server will be stopped.
	 * @throws <code>IllegalArgumentException</code> if state is smaller than 0 or larger than 2.
	 */
	private synchronized void setState(final boolean state)
	{
		if (state)
		{
			if (this.serverPort != null && this.serverPort.isActive())
			{
				return;
			}
			if (this.currentPort != this.nextPort && this.hubURL != null)
			{
				// If the port has changed, the hub must be updated accordingly.
				this.attemptHubKeyUpload(this.hubURL);
			}
			this.currentPort = this.nextPort;
			this.serverPort = new ServerListener(this, this.currentPort);
			this.serverPort.start();
		}
		else
		{
			if (this.clients != null)
			{
				//Terminate all sessions.
				for (final Session session : this.clients)
				{
					this.terminateSession(session);
				}
			}
			
			if (this.serverPort != null)
			{
				this.serverPort.stopListening();
				this.serverPort = null;
			}
		}
	}
	
	/**
	 * @return The port number the server is currently listening on. In case the server is not
	 *         active, it will return the port it was listening on before it was stopped.
	 */
	public int getPortCurrent()
	{
		return this.currentPort;
	}
	
	/**
	 * @return The port number the server will listen on once it's (re-)started.
	 */
	public int getPortNext()
	{
		return this.nextPort;
	}
	
	/**
	 * Sets the port the server shall listen on once it is (re-)started.
	 * 
	 * @param newPort
	 *            The new port to listen on.
	 * @throws IllegalArgumentException
	 *             if <code>JO.isPortValid(newPort)</code> returns <code>false</code>.
	 */
	public void setPort(final int newPort)
	{
		if (!JO.isPortValid(newPort))
		{
			throw new IllegalArgumentException();
		}
		this.nextPort = newPort;
	}
	
	/**
	 * @return Returns whether the server is accepting connections on the specified
	 *         <code>port</code>.
	 */
	public boolean isActive()
	{
		return this.serverPort.isActive();
	}
	
	/**
	 * Causes the server to close all active connections and stop waiting for connections.
	 */
	public void stopListening()
	{
		this.setState(false);
	}
	
	/**
	 * Starts the server, or does nothing if it is already running.
	 */
	public void start()
	{
		this.setState(true);
	}
	
	/**
	 * Forces a restart of the network I/O, closing all connections and applying a new port number
	 * given by <code>setPort</code>. If no new port number was given, the previous one will be
	 * reused.
	 */
	public synchronized void restart()
	{
		this.stopListening();
		this.start();
	}
	
	/**
	 * Sets the password which is required to connect to the server.
	 * 
	 * @param password
	 *            The new password, set it to null to disable password authentication.
	 */
	public void setPassword(final String password)
	{
		this.password = password;
	}
	
	/**
	 * @return The password which is required to connect to this server, or <code>null</code> if no
	 *         password is required.
	 */
	public String getPasword()
	{
		return this.password;
	}
	
	/**
	 * Outputs whether a password is required to connect to this server.
	 * 
	 * @return <code>true</code> if a password is required to connect, otherwise <code>false<code>.
	 */
	public boolean passwordEnabled()
	{
		if (this.password == null)
		{
			return false;
		}
		return true;
	}
	
	/**
	 * @return The maximum amount of players that can simultaneously connect to this server.
	 */
	public int getMaxPlayers()
	{
		return this.maxPlayers;
	}
	
	/**
	 * Sets the maximum amount of players that can simultaneously connect to this server.
	 * 
	 * @param maxPlayers
	 *            The maximum player limit. If the current number of players reaches or exceeds this
	 *            limit, no further client connections will be allowed.
	 * @throws IllegalArgumentException
	 *             if <code>maxPlayers</code> is smaller than 0 or
	 *             larger than 255
	 */
	public void setMaxPlayers(final int maxPlayers)
	{
		if (maxPlayers < 0 || maxPlayers > 255)
		{
			throw new IllegalArgumentException();
		}
		this.maxPlayers = maxPlayers;
	}
	
	/**
	 * Enables hub-supported connection security by generating a key pair (if it hasn't already been
	 * generated) and attempting to upload it to the hub. (see the security scheme for details)
	 * 
	 * @param hubURL
	 *            The URL to which the public key will be uploaded - see
	 *            <code>InitialKeyPair.uploadPublicKey</code> for details.
	 * @return Whether or not the key was successfully uploaded. If not, hub-supported connection
	 *         security won't be used.
	 */
	public boolean initializeHub(final String hubURL)
	{
		if (this.keyPair == null)
		{
			this.keyPair = new InitialKeyPair();
			this.keyPair.generateKeyPair();
		}
		return this.attemptHubKeyUpload(hubURL);
	}
	
	private boolean attemptHubKeyUpload(final String hubURL)
	{
		if (this.keyPair.uploadPublicKey(hubURL, this.currentPort))
		{
			this.hubURL = hubURL;
			return true;
		}
		else
		{
			this.hubURL = null;
			return false;
		}
	}
	
	/**
	 * @return Whether hub-supported connection security is used.
	 */
	public boolean isHubRegistered()
	{
		return this.hubURL != null;
	}
	
	/**
	 * @return The URL to which the public key was uploaded. Will return <code>null</code> if
	 *         hub-supported connection security is disabled.
	 */
	public String hubURL()
	{
		return this.hubURL;
	}
}
